package org.blim.blackdawn.graph;

import java.util.ArrayList;
import java.util.List;

import org.blim.blackdawn.model.Model;
import org.blim.blackdawn.model.PointLight;
import org.blim.blackdawn.shader.Shaders;
import static org.blim.blackdawn.shader.Shaders.VEC4;
import static org.blim.blackdawn.shader.Shaders.MAX_LIGHTS;

import android.opengl.Matrix;

/**
 * Renderer traverses the supplied SceneGraph in a breadth first span, and
 * renders each model object it finds using its associated shader.  
 * 
 * <p>The Renderer's main task before invoking the shader is to calculate the
 * final world co-ordinates of each model object.  The world model matrix
 * for a given object is calculated by combining the individual model and
 * animation matrices of each ancestor frame of reference up to the root 
 * of the tree.</p>
 * 
 * <p>For example, an object, contained in a frame of reference 'frame' which
 * is contained in two further nested frames of reference would calculate its 
 * world model matrix as:</p>
 * 
 * <pre>frame.parent.parent.animationMatrix * frame.parent.parent.modelMatrix * frame.parent.animationMatrix * frame.parent.modelMatrix * frame.animationMatrix * frame.modelMatrix</pre>
 * 
 * <p>Note: Matrix multiplication is non-commutative, so order is 
 * important.</p>
 * 
 * <p>The Renderer also pre-calculates the world position of all point 
 * sources of light.  The light sources relevant to each object are then 
 * passed in to the shader.</p>
 * 
 * @author lee@longlost.info
 */
public class Renderer {
	
	public static final float[] ORIGIN = {0.0f, 0.0f, 0.0f, 1.0f};
	
	public static final float[] IDENTITY = new float[16];  
	
	private final SceneGraph mGraph;
	
	public Renderer(SceneGraph graph) {
		this.mGraph = graph;
	}
	
	public void render() {
		List<ReferenceFrame> workList = new ArrayList<ReferenceFrame>();
		workList.add(mGraph.mWorld);
		
		// Calculate the positions of each light in eye space
		// and prepare the shader arrays
		int lightCount = Math.min(mGraph.mLights.size(), MAX_LIGHTS);
		float[] lightPositions = new float[lightCount * VEC4];
		float[] lightColors = new float[lightCount * VEC4];
		
		for (int i = 0; i < lightCount; i++) {
			ReferenceFrame lightFrame = mGraph.mLights.get(i);
			float[] color = ((PointLight) lightFrame.mModel).mColor;
			float[] lightMatrix = calculateModelMatrix(lightFrame);
			
			Matrix.multiplyMM(lightMatrix, 0, mGraph.mView, 0, lightMatrix, 0);
			System.arraycopy(color, 0, lightColors, i * VEC4, VEC4);
			Matrix.multiplyMV(lightPositions, i * VEC4, lightMatrix, 0, ORIGIN, 0);
		}
		
		// Render each model object that has an associated shader
		while (!workList.isEmpty()) {
			ReferenceFrame frame = workList.remove(0);
			workList.addAll(frame.mChildFrames);
			
			Model model = frame.mModel;
			
			if (null != model) {
				Shaders.draw(
						model, 
						calculateModelMatrix(frame), 
						mGraph.mView, 
						mGraph.mProjection, 
						lightPositions,
						lightColors,
						lightCount);
			}
		}
	}

	private float[] calculateModelMatrix(ReferenceFrame frame) {
		if (null == frame.mModelMatrix) {
			// Base case: this == world frame
			return IDENTITY;
		} else {
			// Pre-multiply each ancestor to the world frame
			float[] result = new float[16];
			System.arraycopy(frame.mModelMatrix, 0, result, 0, result.length);
			ReferenceFrame i = frame.mParent;
			
			if (null != frame.mAnimation) {
				Matrix.multiplyMM(result, 0, frame.mAnimation.getAnimationMatrix(), 0, result, 0);
			}
			
			while (null != i) {
				if (null != i.mModelMatrix) {
					Matrix.multiplyMM(result, 0, i.mModelMatrix, 0, result, 0);
				}
				
				if (null != i.mAnimation) {
					Matrix.multiplyMM(result, 0, i.mAnimation.getAnimationMatrix(), 0, result, 0);
				}
				
				i = i.mParent;
			}
			
			return result;
		}
	}
	
	static {
		Matrix.setIdentityM(IDENTITY, 0);
	}
}
